package data;

import collision.Utils;
import java.awt.Color;
import java.awt.Point;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author sad
 */
public class NavmeshRegion {
    public static double POINT_RADIUS = 2;
    private List<FPoint> points = new ArrayList<>();
    private List<Integer> pointsQueue = new ArrayList<>();
    private List<Triangle> triangles = new ArrayList<>();
    private List<IndexPair> outlines = new ArrayList<>();
    private double x;
    private double y;
    private double width;
    private double height;
    private GridCell[][] cells;
    private int horizontalCellsCount;
    private int verticalCellsCount;
    private Map<NavmeshRegion, RegionConnection> connections;
    private FPoint center;

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 83 * hash + (int) (Double.doubleToLongBits(this.x) ^ (Double.doubleToLongBits(this.x) >>> 32));
        hash = 83 * hash + (int) (Double.doubleToLongBits(this.y) ^ (Double.doubleToLongBits(this.y) >>> 32));
        hash = 83 * hash + (int) (Double.doubleToLongBits(this.width) ^ (Double.doubleToLongBits(this.width) >>> 32));
        hash = 83 * hash + (int) (Double.doubleToLongBits(this.height) ^ (Double.doubleToLongBits(this.height) >>> 32));
        hash = 83 * hash + this.horizontalCellsCount;
        hash = 83 * hash + this.verticalCellsCount;
        return hash;
    }

    public void addPolygonPoint(double x, double y) {
        points.add(new FPoint(x, y));
    }

    public boolean contains(double x, double y) {
        for (Triangle tr : triangles) {
            FPolygon polygon = new FPolygon();
            polygon.addPoint(points.get(tr.getIndex1()));
            polygon.addPoint(points.get(tr.getIndex2()));
            polygon.addPoint(points.get(tr.getIndex3()));
            if (polygon.contains(x, y)) {
                return true;
            }
        }

        return false;
    }

    public GridCell getGridCell(double x, double y) {
        if (x < this.x || x >= this.x + width || y < this.y || y >= this.y + height) {
            return null;
        }

        int xIndex = (int) ((x - this.x) / Params.CELL_SIZE);
        int yIndex = (int) ((y - this.y) / Params.CELL_SIZE);
        return cells[xIndex][yIndex];
    }

    public int getCellsCount() {
        return horizontalCellsCount * verticalCellsCount;
    }

    public int getHorizontalCellsCount() {
        return horizontalCellsCount;
    }

    public int getVerticalCellsCount() {
        return verticalCellsCount;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public double getWidth() {
        return width;
    }

    public double getHeight() {
        return height;
    }

    public Map<NavmeshRegion, RegionConnection> getConnection() {
        return connections;
    }

    public void setConnections(Map<NavmeshRegion, RegionConnection> connections) {
        this.connections = connections;
    }

    /**
     * By Coordinates
     */
    public boolean isActiveCell(double x, double y) {
        if (x < this.x || x >= this.x + width || y < this.y || y >= this.y + height) {
            return false;
        }

        int xIndex = (int) ((x - this.x) / Params.CELL_SIZE);
        int yIndex = (int) ((y - this.y) / Params.CELL_SIZE);
        return cells[xIndex][yIndex].isActive();
    }

    /**
     * By index
     */
    public boolean isActiveCell(int x, int y) {
        if (x < 0 || x >= horizontalCellsCount || y < 0 || y >= verticalCellsCount) {
            return false;
        }

        return cells[x][y].isActive();
    }

    public boolean getIndexOfCell(double x, double y, Point outputPoint) {
        if (x < this.x || x >= this.x + width || y < this.y || y >= this.y + height) {
            return false;
        }

        int xIndex = (int) ((x - this.x) / Params.CELL_SIZE);
        int yIndex = (int) ((y - this.y) / Params.CELL_SIZE);
        outputPoint.x = xIndex;
        outputPoint.y = yIndex;
        return true;
    }

    /**
     * By index. Without validation
     */
    public boolean isActiveCellFast(int x, int y) {
        GridCell gc = cells[x][y];
        gc.setAffectedCell(true);
        return gc.isActive();
    }

    public void clearAffectance() {
        for (int j = 0; j < verticalCellsCount; j++) {
            for (int i = 0; i < horizontalCellsCount; i++) {
                cells[i][j].setAffectedCell(false);
            }
        }
    }

    public void addConnectedPolygon(NavmeshRegion parea, RegionConnection connection) {
        if (connections == null) {
            connections = new HashMap<>();
        }
        connection.setPolygon(parea);
        connections.put(parea, connection);
    }

    public void setOutlines(List<IndexPair> outlines) {
        this.outlines = outlines;
    }

    public List<FPoint> getPoints() {
        return points;
    }

    public List<Integer> getPointsQueue() {
        return pointsQueue;
    }

    public List<Triangle> getTriangles() {
        return triangles;
    }

    public List<IndexPair> getOutlines() {
        return outlines;
    }

    public FRectangle getBoundaries() {
        FRectangle rect = new FRectangle();
        if (!points.isEmpty()) {
            rect.move(points.get(0));
        }

        for (FPoint point : points) {
            rect.add(point);
        }

        return rect;
    }

    protected double findClosestLeftSide(double value) {
        int count = (int) (value / Params.CELL_SIZE);
        return Params.CELL_SIZE * count;
    }

    protected double findClosestRightSide(double value) {
        int count = (int) (value / Params.CELL_SIZE);
        return Params.CELL_SIZE * (count + 1);
    }

    public void updateBoundaries() {
        FRectangle rectangle = getBoundaries();
        x = findClosestLeftSide(rectangle.getMinX());
        y = findClosestLeftSide(rectangle.getMinY());
        width = findClosestRightSide(rectangle.getMaxX()) - x;
        height = findClosestRightSide(rectangle.getMaxY()) - y;
        horizontalCellsCount = (int) Math.round(width / Params.CELL_SIZE);
        verticalCellsCount = (int) Math.round(height / Params.CELL_SIZE);
        updateCells();
        center = new FPoint(rectangle.getCenterX(), rectangle.getCenterY());
    }

    public FPoint getCenter() {
        return center;
    }

    private boolean isCellCollidesWithBorder(double x, double y, double w, double h) {
        for (IndexPair pair : outlines) {
            FPoint p1 = points.get(pair.getIndex1());
            FPoint p2 = points.get(pair.getIndex2());
            if (Utils.linesIntersect(p1.x, p1.y, p2.x, p2.y, x, y, x + w, y)) {
                return false;
            }
            if (Utils.linesIntersect(p1.x, p1.y, p2.x, p2.y, x, y, x, y + h)) {
                return false;
            }
            if (Utils.linesIntersect(p1.x, p1.y, p2.x, p2.y, x, y + h, x + w, y + h)) {
                return false;
            }
            if (Utils.linesIntersect(p1.x, p1.y, p2.x, p2.y, x + w, y, x + w, y + h)) {
                return false;
            }
        }
        return true;
    }

    private Set<Double> getCollissionPointsOfHorizontalRayFromPointToLeftAndBoundaries(double x, double y) {
        Set<Double> cp = new HashSet<>();
        for (IndexPair pair : outlines) {
            FPoint p1 = points.get(pair.getIndex1());
            FPoint p2 = points.get(pair.getIndex2());
            if (p1.x > x && p2.x > x) {
                continue;
            }
            FPoint point = Utils.getLineLineIntersection(p1.x, p1.y, p2.x, p2.y, x, y, -999999999999999.0, y);
            if (point != null) {
                cp.add(point.x);
            }
        }
        return cp;
    }

    protected boolean isCellInPolygonArea(double x, double y, double w, double h) {
        //first we check if cell collides with border of the polygon
        if (!isCellCollidesWithBorder(x, y, w, h)) {
            return false;
        }
        //then we check, how much times ray from cell center collides with
        //boundaries to determine is cell inside or outside
        Set<Double> cpoints = getCollissionPointsOfHorizontalRayFromPointToLeftAndBoundaries(x + w / 2.0, y + h / 2.0);
        if (cpoints == null || (cpoints.size() % 2) == 0) {
            return false;
        }
        return true;
    }

    protected void updateCells() {
        cells = new GridCell[horizontalCellsCount][verticalCellsCount];
        double tx;
        double ty = y;
        for (int j = 0; j < verticalCellsCount; j++, ty += Params.CELL_SIZE) {
            tx = x;
            for (int i = 0; i < horizontalCellsCount; i++, tx += Params.CELL_SIZE) {
                GridCell gc = new GridCell();
                cells[i][j] = gc;
                if (!isCellInPolygonArea(tx, ty, Params.CELL_SIZE, Params.CELL_SIZE)) {
                    gc.setActive(false);
                }
            }
        }
    }

    public void drawGrid(InstrumentGraphics gr) {
        gr.setColor(new Color(200, 200, 200, 20));
        gr.fillRect(x, y, width, height);
        double tx = x;
        gr.setColor(new Color(100, 100, 100, 20));
        for (int i = 0; i < horizontalCellsCount; i++) {
            gr.drawLine(tx, y, tx, y + height);
            tx += Params.CELL_SIZE;
        }
        double ty = y;
        for (int i = 0; i < verticalCellsCount; i++) {
            gr.drawLine(x, ty, x + width, ty);
            ty += Params.CELL_SIZE;
        }
    }

    protected void drawActiveCells(InstrumentGraphics gr) {
        double tx;
        double ty = y;
        Color commonColor = new Color(100, 200, 100, 255);
        Color affectedColor = commonColor.darker();

        for (int j = 0; j < verticalCellsCount; j++, ty += Params.CELL_SIZE) {
            tx = x;
            for (int i = 0; i < horizontalCellsCount; i++, tx += Params.CELL_SIZE) {
                GridCell cell = cells[i][j];
                if (cell.isActive()) {
                    if (cell.isAffectedCell()) {
                        gr.setColor(affectedColor);
                    } else {
                        gr.setColor(commonColor);
                    }
                    gr.fillRect(tx, ty, Params.CELL_SIZE, Params.CELL_SIZE);
                }
            }
        }
    }

    public void drawPolygon(InstrumentGraphics g) {
        g.setColor(Color.green.darker());
        for (Triangle t : triangles) {
            FPolygon polygon = new FPolygon();
            polygon.addPoint(points.get(t.getIndex1()));
            polygon.addPoint(points.get(t.getIndex2()));
            polygon.addPoint(points.get(t.getIndex3()));
            g.fillPolygon(polygon);
        }

        g.setColor(Color.red);
        for (FPoint point : points) {
            g.fillCircle(point.getX(), point.getY(), POINT_RADIUS);
        }

        g.setColor(Color.WHITE);
        for (Integer pointIndex : pointsQueue) {
            g.fillCircle(points.get(pointIndex).getX(), points.get(pointIndex).getY(), POINT_RADIUS);
        }

        g.setColor(Color.WHITE);
        for (IndexPair pair : outlines) {
            g.drawLine(points.get(pair.getIndex1()), points.get(pair.getIndex2()));
        }
    }

    public void draw(InstrumentGraphics g) {
        drawGrid(g);
        drawPolygon(g);
        drawActiveCells(g);
    }

    public boolean checkVisionLine(int x1, int y1, int x2, int y2) {
        int ystep, xstep;    // the step on y and x axis 
        int error;           // the error accumulated during the increment 
        int errorprev;       // *vision the previous value of the error variable 
        int ty = y1, tx = x1;  // the line points 
        int ddy, ddx;        // compulsory variables: the double values of dy and dx 
        int dx = x2 - x1;
        int dy = y2 - y1;
        if (!isActiveCellFast(x1, y1)) {
            return false;
        }

        // NB the last point can't be here, because of its previous point (which has to be verified) 
        if (dy < 0) {
            ystep = -1;
            dy = -dy;
        } else {
            ystep = 1;
        }
        if (dx < 0) {
            xstep = -1;
            dx = -dx;
        } else {
            xstep = 1;
        }

        ddy = 2 * dy;  // work with double values for full precision 
        ddx = 2 * dx;
        if (ddx >= ddy) {  // first octant (0 <= slope <= 1) 
            // compulsory initialization (even for errorprev, needed when dx==dy) 
            errorprev = error = dx;  // start in the middle of the square 
            for (int i = 0; i < dx; i++) {  // do not use the first point (already done) 
                tx += xstep;
                error += ddy;
                if (error > ddx) {  // increment y if AFTER the middle ( > ) 
                    ty += ystep;
                    error -= ddx;
                    // three cases (octant == right->right-top for directions below): 
                    if (error + errorprev < ddx) // bottom square also 
                    {
                        if (!isActiveCellFast(tx, ty - ystep)) {
                            return false;
                        }
                    } else if (error + errorprev > ddx) // left square also 
                    {
                        if (!isActiveCellFast(tx - xstep, ty)) {
                            return false;
                        }
                    } else {  // corner: bottom and left squares also 
                        if (!isActiveCellFast(tx, ty - ystep)) {
                            return false;
                        }
                        if (!isActiveCellFast(tx - xstep, ty)) {
                            return false;
                        }
                    }
                }

                if (!isActiveCellFast(tx, ty)) {
                    return false;
                }

                errorprev = error;
            }
        } else {  // the same as above 
            errorprev = error = dy;
            for (int i = 0; i < dy; i++) {
                ty += ystep;
                error += ddx;
                if (error > ddy) {
                    tx += xstep;
                    error -= ddy;
                    if (error + errorprev < ddy) {
                        if (!isActiveCellFast(tx - xstep, ty)) {
                            return false;
                        }
                    } else if (error + errorprev > ddy) {
                        if (!isActiveCellFast(tx, ty - ystep)) {
                            return false;
                        }
                    } else {
                        if (!isActiveCellFast(tx - xstep, ty)) {
                            return false;
                        }
                        if (!isActiveCellFast(tx, ty - ystep)) {
                            return false;
                        }
                    }
                }

                if (!isActiveCellFast(tx, ty)) {
                    return false;
                }

                errorprev = error;
            }
        }

        return true;
    }
    //8x
    /*private int[][] positions = new int[][]{
     {-1, -1},
     {0, -1},
     {1, -1},
     {-1, 0},
     {1, 0},
     {-1, 1},
     {0, 1},
     {1, 1},
     };*/
    //4x
    private int[][] positions = new int[][]{
        {0, -1},
        {-1, 0},
        {1, 0},
        {0, 1}
    };

    public GridCell getGridCellByIndexFast(int x, int y) {
        return cells[x][y];
    }

    public List<Point> getNeighbours(Point p, List<Point> output) {
        int px = p.x;
        int py = p.y;
        if (px < 0 || px >= horizontalCellsCount || py < 0 || py >= verticalCellsCount) {
            return null;
        }

        for (int[] poss : positions) {
            int tx = px + poss[0];
            int ty = py + poss[1];
            if (tx < 0 || tx >= horizontalCellsCount || ty < 0 || ty >= verticalCellsCount) {
                return null;
            }
            GridCell cell = getGridCellByIndexFast(tx, ty);
            if (cell.isWalkable()) {
                output.add(new Point(tx, ty));
            }
        }
        return output;
    }
}
